/*
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 *
 * The Initial Developer of the Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 * Portions created by Mark A. Kobold are Copyright (C) 2000-2007. All Rights Reserved.
 *
 * Contributor(s): 
 *  Mark A. Kobold [mkobold <at> isqlviewer <dot> com].
 *  
 * If you didn't download this code from the following link, you should check
 * if you aren't using an obsolete version: http://www.isqlviewer.com
 */
package org.isqlviewer.sql;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Properties;
import java.util.TreeSet;

import org.isqlviewer.util.UserProperties;

/**
 * Connection profile to control some statement and connection behavior with the JdbcService class.
 * <p>
 * This is a somewhat typical bean class, with getters/setters for the respective parameters. Each connection profile
 * can be chained together using the {@link #ConnectionProfile(ConnectionProfile)} constructor.
 * <p>
 * The parameters of this class historically were embedded into the actually connection wrapper class. This proved to be
 * extremely cumbersome and fragile. This implementation should proved to be more extensible and does not force to
 * expose any internals of the connection wrapper.
 * <p>
 * This implementation also allows for changes on the fly to the profile or parent profiles without having a reference
 * to the actual connection, and still having a live change event.
 * 
 * @author Mark A. Kobold &lt;mkobold at isqlviewer dot com&gt;
 * @version 1.0
 */
public final class ConnectionProfile implements Serializable {

    /**
     * Boolean property for enabling JDBC tracing through the java.sql.DriverManager mechanisim.
     * <p>
     * 
     * @see java.sql.DriverManager#setLogWriter(java.io.PrintWriter)
     */
    public static final String JDBC_TRACE_ENABLED = "jdbc.trace-enabled";
    /**
     * Boolean property to tell database connection that you want genereated keys if available from ResultSets.
     * <p>
     * 
     * @see java.sql.Statement#getGeneratedKeys()
     */
    public static final String JDBC_GENERATE_KEYS = "jdbc.resultsetkeys-enabled";
    /**
     * Integer property for configuring resultset holdability.
     * 
     * @see java.sql.Connection#setHoldability(int)
     */
    public static final String JDBC_HOLDABILITY = "jdbc.resultset-holdability";
    /**
     * Boolean property to turn on/off escaping processing in the JDBC driver.
     * <p>
     * 
     * @see java.sql.Statement#setEscapeProcessing(boolean)
     */
    public static final String JDBC_ESCAPE_PROC = "jdbc.escapeProc-enabled";
    /**
     * Boolean property for enabling reverse fetching of ResultSet data.
     * <p>
     * 
     * @see java.sql.Statement#setFetchDirection(int)
     */
    public static final String JDBC_REVERSE_FETCHING = "jdbc.reverseFetch-enabled";
    /**
     * Integer property to configure the maximum number of rows a Statement object should return.
     * <p>
     * 
     * @see java.sql.Statement#setMaxRows(int)
     */
    public static final String JDBC_MAX_ROWS = "jdbc.max-rows";
    /**
     * Integer property to configure the maximum field size a Statement object should return.
     * <p>
     * 
     * @see java.sql.Statement#setMaxFieldSize(int)
     */
    public static final String JDBC_MAX_FIELD_SIZE = "jdbc.max-fieldsize";
    /**
     * Boolean property to ensure that the connection is valid and reconnects automatically when queries are executed.
     */
    public static final String CONNECTION_KEEPALIVE = "jdbc.keepalive-enabled";
    /**
     * Integer property for the number of seconds that should pass before a connection times-out when connecting.
     */
    public static final String LOGIN_TIMEOUT = "jdbc.timeout-login";
    /**
     * Integer property for the number of seconds that should pass before a statement execution times-out.
     */
    public static final String QUERY_TIMEOUT = "jdbc.timeout-query";
    /**
     * Boolean property to debug JDBC activity by printing stack traces on exceptions.
     */
    public static final String PRINT_STACKTRACES = "jdbc.debug-enabled";
    /**
     * Integer property to determine how deep to process chained SQLExceptions &amp; SQLWarnings.
     */
    public static final String MAX_EXCEPTION_CHAIN_LENGTH = "jdbc.max-warnings";
    /**
     * String property for declaring a specifc schema to use for schema sensitive operations.
     * <p>
     * By default on most RDBMS the default schema is the user-name for the connection.
     */
    public static final String PREFERRED_SCHEMA = "jdbc.preferred-schema";
    /**
     * String property for declaring a specifc catalog to use for catalog sensitive operations.
     * <p>
     * This will also cause the connection to for a change to this catalog if is not a <tt>null</tt> value.
     */
    public static final String PREFERRED_CATALOG = "jdbc.preferred-catalog";
    /**
     * Boolean property that indicates wheter the storage of the service credentials are un-encrypted.
     */
    public static final String USE_SECURE_CREDENTIALS = "jdbc.encrypt-credentials";
    /**
     * Boolean property that indicates wheter the storage of the service credentials actually occurs.
     */
    public static final String STORE_CREDENTIALS = "jdbc.store-credentials";

    // backing store for settings this makes it easier to export/import in the end //
    // though it doesn't follow the true bean convention, I don't think it matters //
    private UserProperties profileSettings = new UserProperties();
    //
    private TreeSet<File> resources = new TreeSet<File>();
    // notification mechanisim when settings are changed.
    private transient PropertyChangeSupport propertyListeners = new PropertyChangeSupport(this);
    // Classloader reference from the parent for creating hierical classloaders as expected.
    private transient ClassLoader parentClassLoader = ConnectionProfile.class.getClassLoader();

    // UID for serialization compatability purposes.
    private static final long serialVersionUID = 8092402597793653452L;

    /**
     * Creates default connection profile with no default settings.
     * <p>
     */
    public ConnectionProfile() {

        this(null);
    }

    /**
     * Creates a connection profile with default profile.
     * <p>
     * If a profile setting is not available in this instance an attempt to defaults profile will be made.
     * 
     * @param defaults parent profile for settings if not available in this profile; can be <tt>null</tt>.
     */
    public ConnectionProfile(ConnectionProfile defaults) {

        if (defaults != null) {
            profileSettings = new UserProperties(defaults.profileSettings);
            parentClassLoader = defaults.toClassLoader();
        }
    }

    /**
     * Helper method for determining if this profile contains any settings.
     * <p>
     * 
     * @return <tt>true</tt> if all settings from this profile are coming from the defaults.
     */
    public boolean containsSettings() {

        return profileSettings.isEmpty();
    }

    /**
     * Get the maximum number of chained exceptions to show for SQLExceptions.
     * <p>
     * This settings defaults to <tt>10</tt>
     * 
     * @return depth of chained exceptions to expose for SQLExceptions
     * @see java.sql.SQLException#getNextException()
     */
    public long getMaxExceptionLength() {

        return profileSettings.getLong(MAX_EXCEPTION_CHAIN_LENGTH, 10);
    }

    /**
     * Sets the maximum number of chained exceptions to display for SQLExceptions.
     * <p>
     * This method will fire a property change event for {@link #MAX_EXCEPTION_CHAIN_LENGTH}.
     * 
     * @param maxExceptionLength depth of chained exceptions to expose.
     */
    public void setMaxExceptionLength(int maxExceptionLength) {

        int previous = profileSettings.getInt(MAX_EXCEPTION_CHAIN_LENGTH);
        profileSettings.putInt(MAX_EXCEPTION_CHAIN_LENGTH, maxExceptionLength);
        propertyListeners.firePropertyChange(MAX_EXCEPTION_CHAIN_LENGTH, previous, maxExceptionLength);
    }

    /**
     * Sets the preferred escape processing flag for configuring statements.
     * <p>
     * This settings defaults to <tt>false</tt>
     * 
     * @return <tt>true</tt> if escape processing should be done by the driver on queries.
     * @see java.sql.Statement#setEscapeProcessing(boolean)
     */
    public boolean isEscapeProcessing() {

        return profileSettings.getBoolean(JDBC_ESCAPE_PROC, false);
    }

    /**
     * Sets the preferred escape processing flag when configuring statements.
     * <p>
     * This method will fire a property change event for {@link #JDBC_ESCAPE_PROC}.
     * 
     * @param escapeProcessing flag to enable/disable escape processing on the JDBC driver.
     * @see java.sql.Statement#setEscapeProcessing(boolean)
     */
    public void setEscapeProcessing(boolean escapeProcessing) {

        boolean previous = profileSettings.getBoolean(JDBC_ESCAPE_PROC);
        profileSettings.putBoolean(JDBC_ESCAPE_PROC, escapeProcessing);
        propertyListeners.firePropertyChange(JDBC_ESCAPE_PROC, previous, escapeProcessing);
    }

    /**
     * Determines if connections are automatically recovered when disconnected.
     * <p>
     * This settings defaults to <tt>false</tt>
     * 
     * @return <tt>true</tt> if the connection is recovered when disconnected upon statement execution.
     */
    public boolean isKeepAlive() {

        return profileSettings.getBoolean(CONNECTION_KEEPALIVE, false);
    }

    /**
     * Sets the preferred keep-alive parameter for the connection.
     * <p>
     * This method will fire a property change event for {@link #CONNECTION_KEEPALIVE}.
     * 
     * @param keepAlive flag to enable/disable automatically recovering a broken connection.
     */
    public void setKeepAlive(boolean keepAlive) {

        boolean previous = profileSettings.getBoolean(CONNECTION_KEEPALIVE);
        profileSettings.putBoolean(CONNECTION_KEEPALIVE, keepAlive);
        propertyListeners.firePropertyChange(CONNECTION_KEEPALIVE, previous, keepAlive);
    }

    /**
     * Determines if JDBC tracing through the java.sql.DriverManager is enabled.
     * <p>
     * This settings defaults to <tt>false</tt>
     * 
     * @return <tt>true</tt>if tracing through the java.sql.DriverManager is enabled.
     */
    public boolean isTracingEnabled() {

        return profileSettings.getBoolean(JDBC_TRACE_ENABLED, false);
    }

    /**
     * Configures JDBC tracing through the java.sql.DriverManager.
     * <p>
     * This method will fire a property change event for {@link #JDBC_TRACE_ENABLED}.
     * 
     * @param tracingEnabled flag to enable/disable JDBC tracing using the java.sql.DriverManager.
     */
    public void setTracingEnabled(boolean tracingEnabled) {

        boolean previous = profileSettings.getBoolean(JDBC_TRACE_ENABLED);
        profileSettings.putBoolean(JDBC_TRACE_ENABLED, tracingEnabled);
        propertyListeners.firePropertyChange(JDBC_TRACE_ENABLED, previous, tracingEnabled);
    }

    /**
     * Determines if stacktraces should be displayed when exceptions occur.
     * <p>
     * This settings defaults to <tt>false</tt>
     * 
     * @return <tt>true</tt> stacktraces should be displayed when an exception occurs.
     */
    public boolean isDebuggingEnabled() {

        return profileSettings.getBoolean(PRINT_STACKTRACES, false);
    }

    /**
     * Sets stacktraces on exceptions from JDBC operations to be displayed.
     * <p>
     * This method will fire a property change event for {@link #PRINT_STACKTRACES}.
     * 
     * @param debuggingEnabled flag to enable/disable the display of stacktraces from exceptions.
     */
    public void setDebuggingEnabled(boolean debuggingEnabled) {

        boolean previous = profileSettings.getBoolean(PRINT_STACKTRACES);
        profileSettings.putBoolean(PRINT_STACKTRACES, debuggingEnabled);
        propertyListeners.firePropertyChange(PRINT_STACKTRACES, previous, debuggingEnabled);
    }

    /**
     * Get the number of seconds allowed before timing out on creating a connection.
     * <p>
     * This settings defaults to <tt>0</tt>
     * 
     * @return number of seconds to wait before giving up on creating a connection; 0 means wait forever.
     */
    public int getLoginTimeout() {

        return profileSettings.getInt(LOGIN_TIMEOUT, 0);
    }

    /**
     * Sets the number of seconds to wait for a connection to be made.
     * <p>
     * This method will fire a property change event for {@link #LOGIN_TIMEOUT}.
     * 
     * @param loginTimeout number of seconds to wait for a connection to be made; use 0 to wait forever.
     */
    public void setLoginTimeout(int loginTimeout) {

        int previous = profileSettings.getInt(LOGIN_TIMEOUT);
        profileSettings.putInt(LOGIN_TIMEOUT, loginTimeout);
        propertyListeners.firePropertyChange(LOGIN_TIMEOUT, new Integer(previous), new Integer(loginTimeout));
    }

    /**
     * Get the number of seconds allowed before timing out on execution of statements.
     * <p>
     * This settings defaults to <tt>0</tt>
     * 
     * @return number of seconds to wait before giving up statement to finish; 0 means wait forever.
     */
    public int getQueryTimeout() {

        return profileSettings.getInt(QUERY_TIMEOUT, 0);
    }

    /**
     * Sets the number of seconds to wait for a connection to be made.
     * <p>
     * This method will fire a property change event for {@link #QUERY_TIMEOUT}.
     * 
     * @param queryTimeout number of seconds to wait for a statement to execute; use 0 to wait forever.
     */
    public void setQueryTimeout(int queryTimeout) {

        int previous = profileSettings.getInt(QUERY_TIMEOUT);
        profileSettings.putInt(QUERY_TIMEOUT, queryTimeout);
        propertyListeners.firePropertyChange(QUERY_TIMEOUT, new Integer(previous), new Integer(queryTimeout));
    }

    /**
     * Get the connections' preferred maximum field size for a data recieved from statements.
     * <p>
     * This settings defaults to <tt>0</tt>
     * 
     * @return maximum number of bytes allowed to be recieved from a single record; 0 means no limit.
     * @see java.sql.Statement#getMaxFieldSize()
     */
    public long getMaxFieldSize() {

        return profileSettings.getLong(JDBC_MAX_FIELD_SIZE, 0l);
    }

    /**
     * Sets the preferred max field size for data recieved from statements.
     * <p>
     * This method will fire a property change event for {@link #JDBC_MAX_FIELD_SIZE}.
     * 
     * @param maxFieldSize maximum number of bytes to recieve from a single record;0 means no limit.
     * @see java.sql.Statement#setMaxFieldSize(int)
     */
    public void setMaxFieldSize(long maxFieldSize) {

        long previous = profileSettings.getLong(JDBC_MAX_FIELD_SIZE);
        profileSettings.putLong(JDBC_MAX_FIELD_SIZE, maxFieldSize);
        propertyListeners.firePropertyChange(JDBC_MAX_FIELD_SIZE, new Long(previous), new Long(maxFieldSize));
    }

    /**
     * Gets the maximum number of rows to receive from a statement.
     * <p>
     * This settings defaults to <tt>0</tt>
     * 
     * @return maximum number of rows to receive from a statement; 0 means no limit.
     * @see java.sql.Statement#getMaxRows()
     */
    public long getMaxRowCount() {

        return profileSettings.getLong(JDBC_MAX_ROWS, 0l);
    }

    /**
     * Sets the preferred maximum number of rows to recieve from a statement.
     * <p>
     * This method will fire a property change event for {@link #JDBC_MAX_ROWS}.
     * 
     * @param maxRowCount The maxRowCount to set.
     */
    public void setMaxRowCount(long maxRowCount) {

        long previous = profileSettings.getLong(JDBC_MAX_ROWS);
        profileSettings.putLong(JDBC_MAX_ROWS, maxRowCount);
        propertyListeners.firePropertyChange(JDBC_MAX_ROWS, new Long(previous), new Long(maxRowCount));
    }

    /**
     * Sets the parameter for accessing ResultSet keys for each statement.
     * <p>
     * This settings defaults to <tt>false</tt>
     * 
     * @return <tt>true</tt> if generated keys from ResultSet objects are queried for.
     * @see java.sql.Statement#getGeneratedKeys()
     */
    public boolean isResultsetKeys() {

        return profileSettings.getBoolean(JDBC_GENERATE_KEYS, false);
    }

    /**
     * Sets the parameter to access generated keys from executed statements.
     * <p>
     * This method will fire a property change event for {@link #JDBC_GENERATE_KEYS}.
     * 
     * @param resultsetKeys flag to enable/disable generated key acquisition
     * @see java.sql.Statement#getGeneratedKeys()
     */
    public void setResultsetKeys(boolean resultsetKeys) {

        boolean previous = profileSettings.getBoolean(JDBC_GENERATE_KEYS);
        profileSettings.putBoolean(JDBC_GENERATE_KEYS, resultsetKeys);
        propertyListeners.firePropertyChange(JDBC_GENERATE_KEYS, previous, resultsetKeys);
    }

    /**
     * Gets the flag for enabling reverse-fetchings when executing statements.
     * <p>
     * This settings defaults to <tt>false</tt>
     * 
     * @return <tt>true</tt> if statements should use reverse-fetching direction for results.
     * @see java.sql.Statement#setFetchDirection(int)
     */
    public boolean isReverseFetching() {

        return profileSettings.getBoolean(JDBC_REVERSE_FETCHING, false);
    }

    /**
     * Sets the flag to enable reverse-fetching when results are recieve from a statement.
     * <p>
     * This method will fire a property change event for {@link #JDBC_GENERATE_KEYS}.
     * 
     * @param reverseFetching flag to configure fetch direction; <tt>true</tt> = reverse; <tt>false</tt> = forward.
     * @see java.sql.Statement#setFetchDirection(int)
     */
    public void setReverseFetching(boolean reverseFetching) {

        boolean previous = profileSettings.getBoolean(JDBC_REVERSE_FETCHING);
        profileSettings.putBoolean(JDBC_REVERSE_FETCHING, reverseFetching);
        propertyListeners.firePropertyChange(JDBC_REVERSE_FETCHING, previous, reverseFetching);
    }

    /**
     * Gets the preferred schema to use for schema sensitive operations.
     * <p>
     * This settings defaults to <tt>null</tt>
     * 
     * @return <tt>null</tt> if no schema is preferred; schema will be determined by connection.
     */
    public String getPreferredSchema() {

        return profileSettings.get(PREFERRED_SCHEMA, null);
    }

    /**
     * Sets the preferred schema name to use when performing schema sensitve operations.
     * <p>
     * This method will fire a property change event for {@link #PREFERRED_SCHEMA}.
     * 
     * @param preferredSchema preferred schema name to use; null to use default or disable.
     */
    public void setPreferredSchema(String preferredSchema) {

        String previous = profileSettings.get(PREFERRED_SCHEMA, null);
        if (preferredSchema == null) {
            profileSettings.removeProperty(PREFERRED_SCHEMA);
        } else {
            profileSettings.put(PREFERRED_SCHEMA, preferredSchema);
        }
        propertyListeners.firePropertyChange(PREFERRED_SCHEMA, previous, preferredSchema);
    }

    /**
     * Gets the preferred schema to use for schema sensitive operations.
     * <p>
     * This settings defaults to <tt>null</tt>
     * 
     * @return <tt>null</tt> if no schema is preferred; schema will be determined by connection.
     * @see java.sql.Connection#getCatalog()
     */
    public String getPreferredCatalog() {

        return profileSettings.get(PREFERRED_CATALOG, null);
    }

    /**
     * Sets the preferred catalog name to use when performing catalog sensitve operations.
     * <p>
     * If this property is a non-null value, the connection will also attempt to switch to this catalog upon connection
     * to the server.
     * <p>
     * This method will fire a property change event for {@link #PREFERRED_CATALOG}.
     * 
     * @param preferredCatalog preferred catalog name to use; null to use default or disable.
     * @see java.sql.Connection#getCatalog()
     */
    public void setPreferredCatalog(String preferredCatalog) {

        String previous = profileSettings.get(PREFERRED_CATALOG, null);
        profileSettings.put(PREFERRED_CATALOG, preferredCatalog);
        propertyListeners.firePropertyChange(PREFERRED_CATALOG, previous, preferredCatalog);
    }

    /**
     * Sets whether the credentials for a service are persistent with the rest of the configuration.
     * <p>
     * For situations of paranoia or just general security practice of not storing passwords in files this is the
     * setting to turn on/off this behavior.
     * <p>
     * This method will fire a property change event for {@link #STORE_CREDENTIALS}.
     * 
     * @param credentialsPersistent flag to enable/disable storage of service passwords.
     */
    public void setCredentialsPersistent(boolean credentialsPersistent) {

        String previous = profileSettings.get(STORE_CREDENTIALS, null);
        profileSettings.putBoolean(STORE_CREDENTIALS, credentialsPersistent);
        propertyListeners.firePropertyChange(STORE_CREDENTIALS, previous, Boolean.valueOf(credentialsPersistent));

    }

    /**
     * Gets the flag for determining is service passwords are persistent.
     * <p>
     * This settings defaults to <tt>false</tt>
     * 
     * @return <tt>false</tt> if credentials for the service were not persisted.
     */
    public boolean isCredentialsPersistent() {

        return profileSettings.getBoolean(STORE_CREDENTIALS, false);
    }

    /**
     * Sets whether the credentials for a service are encrypted or plain-text.
     * <p>
     * This method will fire a property change event for {@link #USE_SECURE_CREDENTIALS}.
     * 
     * @param credentialsSecure flag to enable/disable the encryption of service passwords.
     */
    public void setCredentialsSecure(boolean credentialsSecure) {

        String previous = profileSettings.get(USE_SECURE_CREDENTIALS, null);
        profileSettings.putBoolean(USE_SECURE_CREDENTIALS, credentialsSecure);
        propertyListeners.firePropertyChange(USE_SECURE_CREDENTIALS, previous, Boolean.valueOf(credentialsSecure));

    }

    /**
     * Gets the flag for determining is service passwords are encrypted.
     * <p>
     * This settings defaults to <tt>true</tt>
     * 
     * @return <tt>true</tt> if credentials for the service are not in plain-text.
     */
    public boolean isCredentialsSecure() {

        return profileSettings.getBoolean(USE_SECURE_CREDENTIALS, true);
    }

    /**
     * Adds a resourceURL as part of creating a custom classloader for a connection.
     * <p>
     * This method will always return false if a null resourceURL is received. All duplicates are discarded and not
     * added.
     * 
     * @param location resource containing runtime resources required for the connection implementation.
     * @return <tt>true</tt> if the resourceURL was sucessfully added.
     */
    public boolean addResource(File location) {

        if (location != null) {
            return resources.add(location);
        }
        return false;
    }

    /**
     * Removes given resourceURL from this profile.
     * <p>
     * 
     * @param location to needs to be removed from this profile.
     * @return <tt>true</tt> if the URL was actually removed.
     */
    public boolean removeResource(File location) {

        if (location != null) {
            return resources.remove(location);
        }
        return false;
    }

    /**
     * Returns a enumeration of the resource URLs defined in this class.
     * <p>
     * 
     * @return collection of all added resource urls prior.
     */
    public Enumeration<File> resourceElements() {

        return Collections.<File> enumeration(resources);
    }

    /**
     * Exports the settings of this profile as a properties object.
     * <p>
     * Each property can be accessed respectively by the constants defined in this class.
     * 
     * @return properties object containing all profile settings.
     */
    public synchronized Properties toProperties() {

        return (Properties) profileSettings.clone();
    }

    /**
     * Create a ClassLoader using the implied classloader of this class.
     * <p>
     * The parent class-loader will either be the classloader of the parent profile when this instance was created, if
     * no parent profile was referenced then the parent classloader will be the classloader for this class.
     * 
     * @return new class loader instance based on resource URLS added to this profile.
     */
    public synchronized ClassLoader toClassLoader() {

        URL[] resourceArray = new URL[resources.size()];
        int i = 0;
        for (File f : resources) {
            try {
                resourceArray[i++] = f.toURL();
            } catch (MalformedURLException ignored) {
            }
        }
        return new URLClassLoader(resourceArray, parentClassLoader);
    }

    /**
     * Add a PropertyChangeListener to the listener list.
     * <p>
     * The listener is registered for all profile setting changes.
     * 
     * @param listener The PropertyChangeListener to be added
     */
    public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {

        propertyListeners.addPropertyChangeListener(listener);
    }

    /**
     * Add a PropertyChangeListener for a specific property.
     * <p>
     * The listener will be invoked only when a call on firePropertyChange names that specific profile setting.
     * 
     * @param propertyName value should be one of the declared constants of this class.
     * @param listener The PropertyChangeListener to be added
     */
    public synchronized void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {

        propertyListeners.addPropertyChangeListener(propertyName, listener);
    }

    /**
     * Remove a PropertyChangeListener from the listener list.
     * <p>
     * This removes a PropertyChangeListener that was registered for all profile settings.
     * 
     * @param listener The PropertyChangeListener to be removed
     */
    public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {

        propertyListeners.removePropertyChangeListener(listener);
    }

    /**
     * Remove a PropertyChangeListener for a specific profile setting.
     * <p>
     * 
     * @param propertyName value should be one of the declared constants of this class.
     * @param listener The PropertyChangeListener to be removed.
     */
    public synchronized void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {

        propertyListeners.removePropertyChangeListener(propertyName, listener);
    }

}
